package com.soft.zosagent.client;


import com.google.gson.Gson;
import com.ibm.jzos.ZFile;
import com.soft.zosagent.common.*;
import com.soft.zosagent.pojo.*;
import com.soft.zosagent.util.EncryptPWD;
import com.soft.zosagent.util.AgentToolUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Options;
import org.apache.commons.lang3.StringUtils;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Semaphore;

import static com.soft.zosagent.util.AgentToolUtil.checkFileType;

/**
 * 
 * @author cinbo@163.com
 *
 */
public class Client {

	private String uniqueId = UUID.randomUUID().toString();
	public static ClientStaticData CLIENT_STATIC_DATA = new ClientStaticData();
	public static class ClientStaticData{
		public OsType osType;
		public OsType ServerOsType;
		public int Return_Code=0;
		public boolean loginStatus=false;

		//专门为了登录准备的同步锁，用户必须先登录登录，才能进行后续的操作。
		//向服务器发送登录请求后，必须得到反馈，才能确定后续操作。
		public Semaphore semaphoreForLogin = new Semaphore(0);

		public ArgsParamConfig  argsParamConfig = new ArgsParamConfig();
	}
	private int rtnCode=0;
	private String rtnMsg;
	public void Client(){
	}
	public String getRtnMsg() {
		return rtnMsg;
	}

	public void setRtnMsg(String rtnMsg) {
		this.rtnMsg = rtnMsg;
	}

	/**
	 * 解析参数分两种情况，第一种是主机的时候，第二种是开放的时候。
	 * 主机的时候，通过解析文本实现。由get put来表示一条命令。user passwd，表示连接的使用的密码
	 */
	//client 端可指定的参数
	public static class ArgsParamConfig{
		private String userName;
		private String password;

		private String hostIp="";

		private int hostPort;


				private LogLevel logLevel = LogLevel.INFO;
		private String logfile=null;
		private List<AgentCmd> cmdList = new ArrayList<>();

		public String getUserName() {
			return userName;
		}

		public void setUserName(String userName) {
			this.userName = userName;
		}

		public String getPassword() {
			return password;
		}

		public void setPassword(String password) {
			this.password = password;
		}

		public String getHostIp() {
			return hostIp;
		}

		public void setHostIp(String hostIp) {
			this.hostIp = hostIp;
		}

		public int getHostPort() {
			return hostPort;
		}

		public void setHostPort(int hostPort) {
			this.hostPort = hostPort;
		}

		public List<AgentCmd> getCmdList() {
			return cmdList;
		}

		public void setCmdList(List<AgentCmd> cmdList) {
			this.cmdList = cmdList;
		}

		public LogLevel getLogLevel() {
			return logLevel;
		}

		public void setLogLevel(LogLevel logLevel) {
			this.logLevel = logLevel;
		}

		public String getLogfile() {
			return logfile;
		}

		public void setLogfile(String logfile) {
			this.logfile = logfile;
		}



	}

	public String getUniqueId(){
		return uniqueId;
	}

	public void initUniqueId(){

	}
	public static OsType getClientOsType(){
		return  CLIENT_STATIC_DATA.osType;
	}

	public static void setClientOsType(OsType osType){
		CLIENT_STATIC_DATA.osType = osType;
	}

	public void setExitCode(int code){
		//-99 是特殊值。当之前是-99，那么接下来的赋值，就直接是0.用来实现 exitcode功能
		if(code == -99){
			rtnCode = 0;
		}else{
			rtnCode = code;
		}
	}

	public  int getExitCode(){
		return rtnCode;
	}

	private EventLoopGroup group=null;
	private ChannelFuture socketClient= null;
	private  final Semaphore semaphore = new Semaphore(1);
		private static String currentAction="";
	public void releaseWorkerLock(){
		semaphore.release();
	}
	public static String getCurrentAction(){
		return currentAction;
	}

	public void connect(int port, String host) throws Exception {
		// 配置客户端NIO线程组
		group = new NioEventLoopGroup();
		final Client myself = this;

			Bootstrap b = new Bootstrap();
			b.group(group).channel(NioSocketChannel.class)
					.option(ChannelOption.TCP_NODELAY, true)
					.handler(new ChannelInitializer<SocketChannel>() {
						@Override
						public void initChannel(SocketChannel ch)
								throws Exception {
							ch.pipeline().addLast("marDecoder", MarshallingCodeCFactory.buildMarshallingDecoder());
							ch.pipeline().addLast("marEncoder",
									MarshallingCodeCFactory
											.buildMarshallingEncoder());
							ch.pipeline().addLast("clientHandler",new ClientHandler(myself));
						}
					});
			// 发起异步连接操作
			socketClient = b.connect(host, port).sync();

			// 等待客户端链路关闭


	}
	
	private boolean  checkCondition(){
		if(!socketClient.channel().isActive()){
			return false;
		}

		try {
			semaphore.acquire();
		}catch (Exception e){
			e.printStackTrace();
			semaphore.release();
			return false;
		}

		if(!socketClient.channel().isActive()){
			semaphore.release();
			return false;
		}

		return true;
	}

	public void close(){
		try {
			semaphore.acquire();
			socketClient.channel().close().sync();
			LoggerPrint.consoleLog("start to close client.",LogLevel.INFO);
			group.shutdownGracefully();
			LoggerPrint.consoleLog("close client finished.",LogLevel.INFO);
		}catch (Exception e){
			e.printStackTrace();
		}
	}


	/**
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {

		
		Client client = new Client();
		client.initUniqueId();
		LoggerPrint.consoleLog("Version:"+Constant.ZOSAGENT_VERSION,LogLevel.INFO);
		//LoggerPrint.consoleLog("this unique id:"+client.getUniqueId(),LogLevel.INFO);
		//设置client的系统类型
		setupClientOsType();



		//开放运行
		try {
			parseCmdArgs(args);
		}catch (Exception e){
			//StringWriter sw = new StringWriter();
			///e.printStackTrace(new PrintWriter(sw, true));
			LoggerPrint.consoleLog("parse args error.msg:"+e.getMessage(),LogLevel.ERROR);
			System.exit(8);
		}


		//setup loglvl
		try {
			client.connect(CLIENT_STATIC_DATA.argsParamConfig.getHostPort(), CLIENT_STATIC_DATA.argsParamConfig.getHostIp());
		}catch (Exception e){
			StringWriter sw = new StringWriter();
			e.printStackTrace(new PrintWriter(sw, true));
			LoggerPrint.consoleLog("connect failed.msg:"+sw.toString(),LogLevel.ERROR);
			System.exit(8);
		}
		boolean isOk=false;
		try {
			isOk = client.login(CLIENT_STATIC_DATA.argsParamConfig.getUserName(), CLIENT_STATIC_DATA.argsParamConfig.getPassword());
		}catch (Exception e){
			StringWriter sw = new StringWriter();
			e.printStackTrace(new PrintWriter(sw, true));
			LoggerPrint.consoleLog("Exception happens when we login. msg:"+sw.toString(),LogLevel.ERROR);
		}
		if(isOk) {
			//执行提交或者查看或者获取作业返回动作
			client.handleCmdAction(buildCmd(CLIENT_STATIC_DATA.argsParamConfig.getCmdList().get(0)));
			client.close();
			System.exit(client.getExitCode());
		}else{
			System.exit(8);
		}

	}
	private static BaseFileInfo buildBaseFileInfo(String filePath,String side)  throws Exception{
		String fileType = checkFileType(filePath);
		BaseFileInfo baseFileInfo=null;
		if(fileType.equals(Constant.UNIX_FILE)){
			baseFileInfo = new UnixFileInfo();
		}else if(fileType.equals(Constant.WINDOWS_FILE)){
			baseFileInfo = new WinFileInfo();
		}else if(fileType.equals(Constant.ZOS_FILE)){
			baseFileInfo= new ZFileInfo();
			ZFileInfo zFileInfo = (ZFileInfo) baseFileInfo;
			zFileInfo.setFileName(filePath);
			AgentToolUtil.parseDsnameAndMember(zFileInfo);

			if(getClientOsType() == OsType.OS390 && side.equalsIgnoreCase("LOCAL")){
				ZFile zFile = new ZFile("//'"+zFileInfo.getDsname()+"'","rb");
				zFileInfo.setRecfm(zFile.getRecfm());
				zFileInfo.setLrecl(zFile.getLrecl());
				zFile.close();
			}
		}else{
			throw new RuntimeException("not support file.");
		}
		baseFileInfo.setFileName(filePath);
		return baseFileInfo;
	}

	private static CmdUnitInfo buildCmd(AgentCmd agentCmd) throws Exception{
		CmdUnitInfo cmdUnitInfo  = new CmdUnitInfo();
		String action = agentCmd.getCmdName();
		cmdUnitInfo.setAction(action.toUpperCase());

		if(StringUtils.isNotEmpty(agentCmd.getLocalFile())) {
			String localFile = agentCmd.getLocalFile().trim();
			BaseFileInfo baseFileInfo = buildBaseFileInfo(localFile,"LOCAL");
			baseFileInfo.setFileCharset(agentCmd.getLocalFileCharset());
			cmdUnitInfo.setLocalFileInfo(baseFileInfo);
		}
		cmdUnitInfo.setJobId(agentCmd.getJobId());
		cmdUnitInfo.setJobName(agentCmd.getJobName());



		return cmdUnitInfo;
	}

	public static void setupClientOsType() {
		if(System.getProperty("os.arch").contains("s390") || System.getProperty("os.name").contains("z/OS")) {
			//如果本机是ZOS主机。则需要对，local文件进行初始化，无论是下载还是上传。
			setClientOsType( OsType.OS390);
		}else if(System.getProperty("os.name").toUpperCase().contains("LINUX")){
			setClientOsType( OsType.LINUX);
		}else if(System.getProperty("os.name").toUpperCase().contains("WINDOWS")){
			setClientOsType( OsType.WINDOWS);
		}else {
			setClientOsType( OsType.OPENOS);
		}
	}



	private static void parseCmdArgs(String[] args) throws Exception {
		CommandLineParser parser = new GnuParser();
		Options options = new Options();
		//登录主机ip和端口
		options.addOption("h", "host", true, "file service host");
		options.addOption("p", "port", true, "file service port");

			//登录用户和密码
		options.addOption("u", null, true, "login user name");
		options.addOption("pwd", null, true, "login password");
		//执行命令
		options.addOption("cmd", "command", true, "command get/put");

		//本地文件或者是远程目标文件
		options.addOption("lfile","localfile",true,"local file");
		options.addOption("rfile","remotefile",true,"remote file");

		options.addOption("jid","jobid",true,"jes2 job id");
		options.addOption("jn","jobname",true,"jes2 job name");

		//force exit(0)
		options.addOption("ec","exitcode",true,"overwrite exit code");

		//agentclt -h 1.1.1.1 -p 2222 -u baysh02 -pwd adfdafdsa  --command=put --localfile=xxx--remotefile=yyyy


		CommandLine commandLine = parser.parse(options, args);//解析参数






			if (commandLine.hasOption("h")) {
				String hostIP = commandLine.getOptionValue("h");
				CLIENT_STATIC_DATA.argsParamConfig.setHostIp(hostIP);
			}
			if (commandLine.hasOption("p")) {
				String hostPort = commandLine.getOptionValue("p");
				int port = 9999;
				try {
					port = Integer.parseInt(hostPort);
				} catch (NumberFormatException e) {
					LoggerPrint.consoleLog("port is invalid. use defalut port 9999.", LogLevel.ERROR);
				} finally {
					CLIENT_STATIC_DATA.argsParamConfig.setHostPort(port);
				}

			}

			if(StringUtils.isEmpty(CLIENT_STATIC_DATA.argsParamConfig.getHostIp())){
				LoggerPrint.consoleLog("host IP not set.", LogLevel.ERROR);
				System.exit(8);
			}


			if (commandLine.hasOption("u")) {
				String user = commandLine.getOptionValue("u");
				CLIENT_STATIC_DATA.argsParamConfig.setUserName(user);
			}
			if (commandLine.hasOption("pwd")) {
				String pwd = commandLine.getOptionValue("pwd");
				CLIENT_STATIC_DATA.argsParamConfig.setPassword(pwd);
			}



			if (commandLine.hasOption("command")) {
				AgentCmd AgentCmd = new AgentCmd();

				String command = commandLine.getOptionValue("command").toUpperCase();
				StringBuffer tmpString = new StringBuffer();
				Constant.cmdList.forEach(s->{
					tmpString.append("(^").append(s).append("$)*");
				});
				String cmds = tmpString.toString();
				if (!command.matches(cmds)){
					throw new RuntimeException("command[" + command + "] is invalid.");
				}
				AgentCmd.setCmdName(command);
				String localfile = "";
				String remotefile = "";



				if (commandLine.hasOption("localfile")) {
					localfile = commandLine.getOptionValue("localfile");
				}

				AgentCmd.setLocalFile(localfile);

				if (commandLine.hasOption("remotefile")) {
					remotefile = commandLine.getOptionValue("remotefile");
				}


				if(commandLine.hasOption("jid")){
					String jobId = commandLine.getOptionValue("jid");
					AgentCmd.setJobId(jobId);
				}
				if(commandLine.hasOption("jn")){
					String jobName = commandLine.getOptionValue("jn");
					AgentCmd.setJobName(jobName);
				}


				checkZosAgentCmdValid(AgentCmd);
				CLIENT_STATIC_DATA.argsParamConfig.getCmdList().add(AgentCmd);
			}

		
		checkArgs();
	}

	private static void checkZosAgentCmdValid(AgentCmd AgentCmd) {
		switch (AgentCmd.getCmdName()){

		}
	}

	private static String cmdMatcherStringPart(){
		StringBuffer tmpString = new StringBuffer();
		Constant.cmdList.forEach(s->{
			tmpString.append("(^").append(s).append(")*");
		});
		return tmpString.toString();
	}

	private static void checkArgs() {
		ArgsParamConfig argsParamConfig = CLIENT_STATIC_DATA.argsParamConfig;

		if(StringUtils.isEmpty(argsParamConfig.getUserName()) || StringUtils.isEmpty(argsParamConfig.getPassword())){
			throw new RuntimeException("user or password is empty.");
		}

		if(StringUtils.isEmpty(argsParamConfig.getHostIp())){
			throw new RuntimeException("host ip is empty.");
		}

	}

	/**
	 * cmd line中指定了加密的password，则需要先解密。
	 * @param encpwd
	 * @return
	 */
	public static String decryptPassword(String encpwd) {
		return EncryptPWD.decryptPwd(encpwd);
	}

	public void handleCmdAction(CmdUnitInfo cmdUnitInfo) throws Exception{
		if(!checkCondition()){
			return;
		}
		if(cmdUnitInfo.getAction().equalsIgnoreCase(Constant.SENDJOB_ACTION)){
			sendMainFrameJclJob(cmdUnitInfo);
		}else if(cmdUnitInfo.getAction().equalsIgnoreCase(Constant.QUERYJOB_ACTION)){
			queryMainFrameJob(cmdUnitInfo);
		}else if(cmdUnitInfo.getAction().equalsIgnoreCase(Constant.RECVJOB_ACTION)){
			recvMainFrameJobOutput(cmdUnitInfo);
		}else if(cmdUnitInfo.getAction().equalsIgnoreCase(Constant.CANCELJOB_ACTION)){
			cancelMainFrameJobOutput(cmdUnitInfo);
		}
		else{
			LoggerPrint.consoleLog("not support action:"+ cmdUnitInfo.getAction(),LogLevel.ERROR);
			throw new RuntimeException("not support action:"+ cmdUnitInfo.getAction());
		}
	}

	private void cancelMainFrameJobOutput(CmdUnitInfo cmdUnitInfo) throws Exception {
		Client.currentAction = cmdUnitInfo.getAction();

		Request request = new Request();

		//request.setFileInfo(cmdUnitInfo.getLocalFileInfo());
		request.setOsType(getClientOsType());
		request.setAction(ACTION.ACTION_CANCELJOB);
		request.setJobId(cmdUnitInfo.getJobId());
		request.setJobName(cmdUnitInfo.getJobName());
		LoggerPrint.consoleLog("request data:"+ (new Gson()).toJson(request),LogLevel.DEBUG);

		socketClient.channel().writeAndFlush(request).sync();
		LoggerPrint.consoleLog(String.format("request to cancel job[%s(%s)] output to server", cmdUnitInfo.getJobName(),cmdUnitInfo.getJobId()),LogLevel.INFO);
	}

	private void sendMainFrameJclJob(CmdUnitInfo cmdUnitInfo) throws Exception {
		Client.currentAction = cmdUnitInfo.getAction();

//		if(getServerOsType() != OsType.OS390){
//			throw new RuntimeException("server is not z/OS. can't send job.");
//		}
		Request request = new Request();


		if(cmdUnitInfo.getLocalFileInfo().getFileType() ==  Constant.ZOS_FILE){

		}else {
			//读取作业文件内容
			List<String> jobJclList = readJclList(cmdUnitInfo.getLocalFileInfo());
			request.setJobJclList(jobJclList);
		}

		request.setFileInfo(cmdUnitInfo.getLocalFileInfo());
		request.setOsType(getClientOsType());
		request.setAction(ACTION.ACTION_SENDJOB);
		LoggerPrint.consoleLog("request data:"+ (new Gson()).toJson(request),LogLevel.DEBUG);

		socketClient.channel().writeAndFlush(request).sync();
		LoggerPrint.consoleLog(String.format("request to sendjob file[%s] to server", cmdUnitInfo.getLocalFileInfo().getFileName()),LogLevel.INFO);

	}

	private void queryMainFrameJob(CmdUnitInfo cmdUnitInfo)throws Exception{
		Client.currentAction = cmdUnitInfo.getAction();

		Request request = new Request();

		request.setOsType(getClientOsType());
		request.setAction(ACTION.ACTION_QRYJOB);
		request.setJobId(cmdUnitInfo.getJobId());
		request.setJobName(cmdUnitInfo.getJobName());
		LoggerPrint.consoleLog("request data:"+ (new Gson()).toJson(request),LogLevel.DEBUG);

		socketClient.channel().writeAndFlush(request).sync();
		LoggerPrint.consoleLog(String.format("request to query job[%s(%s)] status to server", cmdUnitInfo.getJobName(),cmdUnitInfo.getJobId()),LogLevel.INFO);

	}
	private void recvMainFrameJobOutput(CmdUnitInfo cmdUnitInfo) throws Exception{
		Client.currentAction = cmdUnitInfo.getAction();

//		if(getServerOsType() != OsType.OS390){
//			throw new RuntimeException("server is not z/OS. can't send job.");
//		}
		Request request = new Request();

		//request.setFileInfo(cmdUnitInfo.getLocalFileInfo());
		request.setOsType(getClientOsType());
		request.setAction(ACTION.ACTION_RECVJOB);
		request.setJobId(cmdUnitInfo.getJobId());
		request.setJobName(cmdUnitInfo.getJobName());
		LoggerPrint.consoleLog("request data:"+ (new Gson()).toJson(request),LogLevel.DEBUG);

		socketClient.channel().writeAndFlush(request).sync();
		LoggerPrint.consoleLog(String.format("request to get job[%s(%s)] output to server", cmdUnitInfo.getJobName(),cmdUnitInfo.getJobId()),LogLevel.INFO);

	}
	private List<String> readJclList(BaseFileInfo localFileInfo) {

		return AgentToolUtil.readJclFileLines(localFileInfo.getFileName());

	}

	private static OsType  getServerOsType() {
		return CLIENT_STATIC_DATA.ServerOsType;
	}

	public boolean login(String username,String password) throws Exception{
		Client.currentAction=Constant.LOGIN_ACTION;
		Request request = new Request();
		request.setOsType(getClientOsType());
		request.setUniqueId(uniqueId);

		LoggerPrint.consoleLog(String.format("login to server[%s]", CLIENT_STATIC_DATA.argsParamConfig.getHostIp()),LogLevel.INFO);
		request.setAction(ACTION.ACTION_USER_LOGIN);
		request.setOsType(Client.getClientOsType());
		request.setUserName(username);
		//String pwdString = "";//EncryptPWD.decryptPwd(password);
		request.setUserPwd(password);
		LoggerPrint.consoleLog("request data:"+ (new Gson()).toJson(request),LogLevel.DEBUG);
		socketClient.channel().writeAndFlush(request).sync();

		CLIENT_STATIC_DATA.semaphoreForLogin.acquire();

		if(!CLIENT_STATIC_DATA.loginStatus){
			LoggerPrint.consoleLog("login failed.",LogLevel.INFO);
			return false;
		}
		LoggerPrint.consoleLog("login successfully.",LogLevel.INFO);
		return true;
	}
	public boolean isConnected(){
		if(socketClient == null){
			return false;
		}

		return socketClient.channel().isActive();
	}
}
